🚀 Hosting Additional Sites on Nginx Server

Complete Guide to WordPress Multi-Site Configuration with Security & Optimization

angelscript

📋 Table of Contents

  • Introduction & Overview
  • Bash Script Automation
  • DNS Configuration
  • Site Directory Setup
  • WordPress Installation
  • Security Hardening
  • Performance Optimization
  • FastCGI Caching Configuration
  • SSL/TLS Certificate Setup

📖 Introduction

This comprehensive guide covers the process of hosting additional WordPress sites on an Nginx server. We'll be setting up two different types of sites:

📌 Important Note: This guide assumes you have already set up your first WordPress site on the server. If you need more detailed explanations on any step, please refer to the initial site setup documentation.

🔄 Complete Workflow Overview

1 DNS Configuration - Set up A and CNAME records
2 Bash Scripts Creation - Automate setup processes
3 Directory Structure - Create site directories
4 Database Setup - Create WordPress database
5 Nginx Configuration - Configure server blocks
6 WordPress Installation - Install and configure WP
7 Security Hardening - PHP pools, permissions, restrictions
8 SSL/TLS Setup - Secure with Let's Encrypt
9 Performance Optimization - Caching and tuning

🛠️ Section 1: Bash Script Automation

To streamline the process of hosting additional sites, we'll create automated bash scripts. These scripts will handle repetitive tasks and reduce the possibility of human error.

Creating the Scripts Directory

First, navigate to your home directory and create a dedicated folder for WordPress bash scripts:

cd
mkdir wp_bash_scripts/
cd wp_bash_scripts/

Script 1: Site Directory Creation

This script automatically creates the necessary directory structure for a new WordPress site.

nano 1.create_wp_directories.sh
#!/bin/bash Function to create site directories create_site_directories() { domain=$1 # Check if the directory already exists if [ ! -d "/var/www/$domain" ]; then # Create the directories with appropriate permissions sudo mkdir -p "/var/www/$domain/public_html" sudo mkdir -p "/var/www/$domain/tmp" echo "Site directory for $domain created successfully." else echo "Site directory for $domain already exists." fi } Main script starts here Ask for domain name read -p "Enter domain name: " domain_name Call function to create site directories create_site_directories "$domain_name"
angelscript
💡 What This Script Does:
  • Checks if the site directory already exists
  • Creates /var/www/domain/public_html for website files
  • Creates /var/www/domain/tmp for temporary files
  • Provides confirmation messages

Script 2: Database Creation

This script automates the WordPress database setup with random credentials for enhanced security.

nano 2.create_wp_database.sh
#!/bin/bash echo "DO NOT USE A PERIOD IN THE DATABASE NAME" echo "use an underscore in place of a period" echo "What is your domain name?" read domain DB="$domain" create random database user DBUSER="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1)" create random password PASSWORD="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1)" create random database prefix PREFIX="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 4 | head -n 1)" mysql < angelscript
⚠️ Critical Warning: DO NOT use periods (.) in database names. Always use underscores (_) instead. For example, use example_com instead of example.com
✅ Security Features:
  • Randomly generated 12-character database username
  • Randomly generated 12-character password
  • Random 4-character table prefix (helps prevent SQL injection attacks)
  • Credentials displayed only once - save them immediately!

Script 3: Admin Credentials & WordPress Constants

This script generates secure admin credentials and WordPress security salts.

nano 3.admin_credentials.sh
#!/bin/bash Generate and display WordPress admin username and password echo "Randomly Generated 30 Character WordPress Administrative Credentials" echo "" echo "WordPress Admin Username: $(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 30 | head -n 1)" echo "WordPress Admin Password: $(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 30 | head -n 1)" Fetch WordPress salts wp_salts=$(curl -s https://api.wordpress.org/secret-key/1.1/salt/) Add WP constants wp_constants=$(cat << 'EOF' /** Allow Direct Updating Without FTP / define('FS_METHOD', 'direct' ); /* Disable Editing of Themes and Plugins Using the Built In Editor / define('DISALLOW_FILE_EDIT', 'true' ); /* TURN OFF AUTOMATIC UPDATES */ define('WP_AUTO_UPDATE_CORE', 'false' ); EOF ) Output the salts and constants echo "" echo "WordPress Salts" echo "" echo "$wp_salts" echo "" echo "Constants to be added to wp-config.php" echo "" echo "$wp_constants"
angelscript
💡 Script Features:
  • 30-character credentials: Maximum security for admin access
  • WordPress Salts: Fetched directly from WordPress API
  • Security Constants: Pre-configured for best practices

Script 4: Nginx Server Block Creation

This comprehensive script creates the initial Nginx configuration for your site.

nano 4.nginx_server_block.sh
#!/bin/bash Function to create directories and files if they don't exist create_files_and_directories() { # Check if the includes directory exists, if not create it if [ ! -d "/etc/nginx/includes" ]; then sudo mkdir -p /etc/nginx/includes fi # Check if fastcgi_optimize.conf exists, if not create it if [ ! -f "/etc/nginx/includes/fastcgi_optimize.conf" ]; then sudo tee /etc/nginx/includes/fastcgi_optimize.conf > /dev/null <<EOF fastcgi_connect_timeout 60; fastcgi_send_timeout 180; fastcgi_read_timeout 180; fastcgi_buffer_size 512k; fastcgi_buffers 512 16k; fastcgi_busy_buffers_size 1m; fastcgi_temp_file_write_size 4m; fastcgi_max_temp_file_size 4m; fastcgi_intercept_errors on; EOF fi # Check if browser_caching.conf exists, if not create it if [ ! -f "/etc/nginx/includes/browser_caching.conf" ]; then sudo tee /etc/nginx/includes/browser_caching.conf > /dev/null <<EOF location ~* .(webp|3gp|gif|jpg|jpeg|png|ico|wmv|avi|asf|asx|mpg|mpeg|mp4|pls|mp3|mid|wav|swf|flv|exe|zip|tar|rar|gz|tgz|bz2|uha|7z|doc|docx|xls|xlsx|pdf|iso)$ { add_header Cache-Control "public, no-transform"; access_log off; expires 365d; } location ~* .(js)$ { add_header Cache-Control "public, no-transform"; access_log off; expires 30d; } location ~* .(css)$ { add_header Cache-Control "public, no-transform"; access_log off; expires 30d; } location ~* .(eot|svg|ttf|woff|woff2)$ { add_header Cache-Control "public, no-transform"; access_log off; expires 30d; } EOF fi } Function to create domain.com.conf file create_domain_conf_file() { domain=$1 filename="/etc/nginx/sites-available/$domain.conf" perl # Define server_name without checking for subdomain server_name="$domain www.$domain" # Check if the domain.com.conf file exists, if not create it if [ ! -f "$filename" ]; then sudo tee "$filename" > /dev/null <<EOF server { awk listen 80; server_name $server_name; root /var/www/$domain/public_html; index index.php; location / { try_files \$uri \$uri/ /index.php\$is_args\$args; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; include /etc/nginx/includes/fastcgi_optimize.conf; } include /etc/nginx/includes/browser_caching.conf; access_log /var/log/nginx/access_$domain.log combined buffer=256k flush=60m; error_log /var/log/nginx/error_$domain.log; } EOF fi awk # Create a symbolic link to the sites-enabled directory sudo ln -s /etc/nginx/sites-available/$domain.conf /etc/nginx/sites-enabled/ } Main script starts here Ask for domain name read -p "Enter domain name: " domain_name Call function to create directories and files create_files_and_directories Call function to create domain.com.conf file create_domain_conf_file "$domain_name" Display success message echo "Configuration files created successfully."
angelscript
📌 What This Script Creates:
  • /etc/nginx/includes/fastcgi_optimize.conf - FastCGI performance settings
  • /etc/nginx/includes/browser_caching.conf - Browser cache rules for static assets
  • /etc/nginx/sites-available/domain.conf - Main server block configuration
  • Symbolic link - Links configuration to sites-enabled directory

Making Scripts Executable

Before running any scripts, you must grant them executable permissions:

chmod +x 1.create_wp_directories.sh 2.create_wp_database.sh 3.admin_credentials.sh 4.nginx_server_block.sh

🌐 Section 2: DNS Configuration

Before proceeding with site setup, you must configure DNS records to point your domain to your server.

Required DNS Records

Record Type Name Content/Target Proxy Status
A Record @ (or domain.com) Your Server IP Address DNS Only (initially)
CNAME Record www domain.com DNS Only (initially)
💡 Cloudflare Users: Set proxy status to "DNS Only" initially. You can enable "Proxied" status after SSL certificates are installed and working correctly.

Verifying DNS Resolution

After configuring DNS records, verify that your domain resolves correctly to your server:

ping -c 2 example.com
ping -c 2 www.example.com
✅ Expected Result: Both commands should display your server's IP address, confirming DNS propagation is complete.

📁 Section 3: Creating Site Directories

Now that DNS is configured and our scripts are ready, let's create the directory structure for the new site.

Navigate to Scripts Directory

cd ~/wp_bash_scripts/

Run Directory Creation Script

./1.create_wp_directories.sh
💡 Script Execution: When prompted, enter your domain name exactly as configured in DNS (e.g., example.com). The script will create:
  • /var/www/example.com/public_html/ - Web root directory
  • /var/www/example.com/tmp/ - Temporary files directory

🗄️ Section 4: Database Setup

Create a dedicated MySQL database with secure, randomly generated credentials.

Run Database Creation Script

./2.create_wp_database.sh
⚠️ Critical: When entering the domain name, replace periods with underscores:
  • Correct: example_com
  • Incorrect: example.com
✅ Save These Credentials Immediately: The script will display:
  • Database Name
  • Database Username (random 12 characters)
  • Database Password (random 12 characters)
  • Table Prefix (random 4 characters)

Generate Admin Credentials

./3.admin_credentials.sh
💡 What You'll Receive:
  • Random 30-character WordPress admin username
  • Random 30-character WordPress admin password
  • WordPress security salts (8 unique keys)
  • Recommended wp-config.php constants

⚙️ Section 5: Nginx Server Block Configuration

Create the Nginx configuration that will serve your WordPress site.

Run Server Block Creation Script

./4.nginx_server_block.sh
When prompted, enter your domain name (e.g., example.com). The script will automatically configure server blocks for both example.com and www.example.com.

Test and Reload Nginx

After creating the server block, always test the configuration before reloading:

sudo nginx -t
✅ Expected Output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok

nginx: configuration file /etc/nginx/nginx.conf test is successful
angelscript
sudo systemctl reload nginx

📦 Section 6: WordPress Installation

Now we'll download, configure, and install WordPress for the new site.

Download WordPress

cd ~
wget https://wordpress.org/latest.tar.gz
tar xf latest.tar.gz
cd wordpress/

Configure wp-config.php

mv wp-config-sample.php wp-config.php
nano wp-config.php
💡 Required Changes in wp-config.php:
  1. Database Name: Enter the database name from the script output
  2. Database User: Enter the random username generated
  3. Database Password: Enter the random password generated
  4. Database Prefix: Change $table_prefix to the random prefix + underscore
  5. Authentication Keys & Salts: Replace with the salts from script output
  6. Security Constants: Add the constants provided by the script

Copy Files to Site Directory

cd ~
rsync -artv wordpress/ /var/www/example.com/public_html/

Set Proper Ownership

cd /var/www/example.com/
sudo chown -R www-data:www-data public_html/
ls -l

Complete Installation via Browser

Open your web browser and navigate to http://example.com to complete the WordPress installation through the web interface.

📌 Installation Steps:
  1. Select your language
  2. Enter site title, admin username (from script), admin password (from script), and admin email
  3. Complete the installation
  4. Log in to WordPress dashboard

🔒 Section 7: Security Hardening

Implementing multiple layers of security to protect your WordPress installation.

PHP-FPM Pool Configuration

Creating isolated PHP processes for each site enhances security and performance.

Create Dedicated User

sudo useradd example_user
sudo usermod -a -G example_user www-data
sudo usermod -a -G www-data example_user
sudo usermod -a -G $USER example_user

Create PHP Pool Configuration

cd /etc/php/8.3/fpm/pool.d/
sudo cp www.conf example.com.conf
sudo nano example.com.conf
; pool name [example] ; Username and Group user = example_user group = example_user ; Path and Unix Socket Filename - unique to this pool listen = /run/php/php8.3-fpm-example.com.sock ; Set open file descriptor rlimit. rlimit_files = 15000 ; Set max core size rlimit. rlimit_core = 100 ; PHP POOL CONFIGURATION php_flag[display_errors] = off php_admin_value[error_log] = /var/log/fpm-php.example.log php_admin_flag[log_errors] = on
angelscript

Create PHP-FPM Log File

sudo touch /var/log/fpm-php.example.log
sudo chown example_user:www-data /var/log/fpm-php.example.log
sudo chmod 660 /var/log/fpm-php.example.log

Update Nginx Configuration

sudo nano /etc/nginx/sites-available/example.com.conf
💡 Modify the PHP location block: Change the fastcgi_pass directive to use the new socket:
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;

Test and Reload Services

sudo nginx -t
sudo systemctl reload nginx
sudo systemctl reload php8.3-fpm

File Permissions & Ownership

Set Ownership

cd /var/www/example.com/
sudo chown -R example_user:example_user public_html/ tmp/

Initial Permissions (for updates)

sudo chmod 770 public_html/
sudo chmod 770 tmp/
sudo find /var/www/example.com/public_html/ -type d -exec chmod 770 {} \;
sudo find /var/www/example.com/public_html/ -type f -exec chmod 660 {} \;

Hardened Permissions (for production)

sudo find /var/www/example.com/public_html/ -type d -exec chmod 550 {} \;
sudo find /var/www/example.com/public_html/ -type f -exec chmod 440 {} \;
sudo find /var/www/example.com/public_html/wp-content/ -type d -exec chmod 770 {} \;
sudo find /var/www/example.com/public_html/wp-content/ -type f -exec chmod 660 {} \;
⚠️ Permission Levels:
  • 770/660: Allows WordPress dashboard updates (loosen before updates)
  • 550/440: Read-only for enhanced security (tighten after updates)
  • wp-content always 770/660: Required for uploads and plugin/theme operations

Disable Dangerous PHP Functions

sudo nano /etc/php/8.3/fpm/pool.d/example.com.conf
; DISABLED FUNCTIONS php_admin_value[disable_functions] = shell_exec, opcache_get_configuration, opcache_get_status, disk_total_space, diskfreespace, dl, exec, passthru, pclose, pcntl_alarm, pcntl_exec, pcntl_fork, pcntl_get_last_error, pcntl_getpriority, pcntl_setpriority, pcntl_signal, pcntl_signal_dispatch, pcntl_sigprocmask, pcntl_sigtimedwait, pcntl_sigwaitinfo, pcntl_strerror, pcntl_waitpid, pcntl_wait, pcntl_wexitstatus, pcntl_wifcontinued, pcntl_wifexited, pcntl_wifsignaled, pcntl_wifstopped, pcntl_wstopsig, pcntl_wtermsig, popen, posix_getpwuid, posix_kill, posix_mkfifo, posix_setpgid, posix_setsid, posix_setuid, posix_uname, proc_close, proc_get_status, proc_nice, proc_open, proc_terminate, show_source, system
angelscript
sudo systemctl reload php8.3-fpm

Open_basedir Restrictions

Restricting PHP file access to only necessary directories prevents unauthorized file access.

sudo nano /etc/php/8.3/fpm/pool.d/example.com.conf
php_admin_value[upload_tmp_dir] = /var/www/example.com/tmp/ php_admin_value[sys_temp_dir] = /var/www/example.com/tmp/ php_admin_value[open_basedir] = /var/www/example.com/public_html/:/var/www/example.com/tmp/
angelscript

Database Privileges Restriction

Limit database user privileges to only what WordPress needs.

Verify Database Credentials

sudo grep DB_NAME /var/www/example.com/public_html/wp-config.php
sudo grep DB_USER /var/www/example.com/public_html/wp-config.php

Restrict Privileges

mysql
REVOKE ALL PRIVILEGES ON database_name.* FROM 'username'@'localhost'; GRANT SELECT, INSERT, UPDATE, DELETE ON database_name.* TO 'username'@'localhost'; FLUSH PRIVILEGES; EXIT;
angelscript
💡 Why Restrict Privileges? WordPress only needs SELECT, INSERT, UPDATE, and DELETE for normal operation. Removing CREATE, DROP, and ALTER privileges prevents malicious code from modifying database structure.

🔐 Section 8: SSL/TLS Certificate Configuration

Secure your site with free Let's Encrypt SSL certificates.

Obtain SSL Certificate

sudo certbot certonly --webroot -w /var/www/example.com/public_html/ -d example.com -d www.example.com

Create SSL Configuration File

sudo nano /etc/nginx/ssl/ssl_certs_example.com.conf
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; SSL STAPLING ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
angelscript

Update Server Block for HTTPS

sudo nano /etc/nginx/sites-available/example.com.conf
server { listen 80; server_name example.com www.example.com; # 301 Permanent Redirect to HTTPS return 301 https://example.com$request_uri; } server { listen 443 ssl; http2 on; gradle listen 443 quic; http3 on; server_name example.com www.example.com; root /var/www/example.com/public_html; index index.php; # Include SSL configurations include /etc/nginx/ssl/ssl_certs_example.com.conf; include /etc/nginx/ssl/ssl_all_sites.conf; location / { try_files $uri $uri/ /index.php$is_args$args; } # Place HTTP headers ABOVE php processing block include /etc/nginx/includes/http_headers.conf; location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_param HTTP_HOST $host; fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; } include /etc/nginx/includes/browser_caching.conf; access_log /var/log/nginx/access_example.com.log combined buffer=256k flush=60m; error_log /var/log/nginx/error_example.com.log; }
angelscript

Test and Reload

sudo nginx -t
sudo systemctl reload nginx

Verify SSL Configuration

Test your redirects and HTTPS configuration:

curl -I http://example.com
curl -I http://www.example.com
curl -I https://www.example.com
curl -I https://example.com
✅ Expected Results: All HTTP requests should show 301 redirects to https://example.com

SSL Testing Resources

📌 Test Your SSL Configuration:

⚡ Section 9: Performance Optimization

WordPress Core Optimizations

Disable Post Revisions

Add to wp-config.php:

define('WP_POST_REVISIONS', false);

Increase Memory Limit

Add to wp-config.php:

/** MEMORY LIMIT */ define('WP_MEMORY_LIMIT', '256M');
angelscript

Update PHP pool configuration:

sudo nano /etc/php/8.3/fpm/pool.d/example.com.conf
php_admin_value[memory_limit] = 256M

Optimize WP-Cron

Disable default WP-Cron in wp-config.php:

/** DISABLE WP-CRON */ define('DISABLE_WP_CRON', true);
angelscript

Set up system cron job:

crontab -e
*/15 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

OPcache Configuration

sudo nano /etc/php/8.3/fpm/pool.d/example.com.conf
; OPCACHE CONFIGURATION - DEVELOPMENT SERVER php_admin_value[opcache.memory_consumption] = 256 php_admin_value[opcache.interned_strings_buffer] = 32 php_admin_value[opcache.max_accelerated_files] = 20000 php_admin_flag[opcache.validate_timestamps] = 1 php_admin_value[opcache.revalidate_freq] = 2 php_admin_flag[opcache.validate_permission] = 1 ; OPCACHE CONFIGURATION - PRODUCTION SERVER ;php_admin_value[opcache.memory_consumption] = 256 ;php_admin_value[opcache.interned_strings_buffer] = 32 ;php_admin_value[opcache.max_accelerated_files] = 20000 ;php_admin_flag[opcache.validate_timestamps] = 0 ;php_admin_flag[opcache.validate_permission] = 1
angelscript
💡 OPcache Settings:
  • Development: validate_timestamps = 1 (checks for file changes)
  • Production: validate_timestamps = 0 (maximum performance, manual cache clearing required)

FastCGI Caching

Configure Global FastCGI Cache

sudo nano /etc/nginx/nginx.conf
### FASTCGI CACHING fastcgi_cache_path directive - PATH & NAME must be unique for each site fastcgi_cache_path /var/run/example_cache levels=1:2 keys_zone=EXAMPLECACHE:100m inactive=60m; fastcgi_cache_path /var/run/example2_cache levels=1:2 keys_zone=EXAMPLE2CACHE:100m inactive=60m; applied to all sites fastcgi_cache_key "$scheme$request_method$host$request_uri"; fastcgi_cache_use_stale error timeout invalid_header http_500; fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
angelscript

Update Server Block for Caching

sudo nano /etc/nginx/sites-available/example.com.conf
location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_param HTTP_HOST $host; fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; # fastcgi caching directives fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; fastcgi_cache EXAMPLECACHE; fastcgi_cache_valid 60m; } include /etc/nginx/includes/fastcgi_cache_excludes.conf; add_header X-FastCGI-Cache $upstream_cache_status; Cache purge location location ~ /purge(/.*) { fastcgi_cache_purge EXAMPLECACHE "$scheme$request_method$host$1"; }
angelscript
sudo nginx -t
sudo systemctl reload nginx

Rate Limiting

Protect wp-login.php and xmlrpc.php from brute force attacks.

cd /etc/nginx/includes/
sudo cp rate_limiting_example.com.conf rate_limiting_example.net.conf
sudo nano rate_limiting_example.net.conf
location = /wp-login.php { limit_req zone=wp burst=20 nodelay; limit_req_status 444; include snippets/fastcgi-php.conf; fastcgi_param HTTP_HOST $host; fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; } location = /xmlrpc.php { limit_req zone=wp burst=20 nodelay; limit_req_status 444; include snippets/fastcgi-php.conf; fastcgi_param HTTP_HOST $host; fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; }

Include in server block:

# Rate Limiting Include include /etc/nginx/includes/rate_limiting_example.net.conf;
angelscript

🔧 Section 10: Maintenance Scripts

Script: Loosen Permissions (for updates)

nano ~/wp_bash_scripts/5.loosen_permissions.sh
#!/bin/bash List directory contents of /var/www for reference sudo ls -l /var/www Prompt for the domain name read -p "Enter the domain name: " domain_name Check if the domain directory exists if [ ! -d "/var/www/$domain_name/public_html/" ]; then echo "Error: Domain directory /var/www/$domain_name/public_html/ does not exist." exit 1 fi Set permissions with confirmation echo "Setting permissions for /var/www/$domain_name/public_html/..." Grant read, write, and execute permissions to owner and group - directories sudo find "/var/www/$domain_name/public_html/" -type d -exec chmod 770 {} ; Grant read and write permissions to owner and group - files sudo find "/var/www/$domain_name/public_html/" -type f -exec chmod 660 {} ; echo "Permissions set to allow WordPress updates using the dashboard!"
angelscript

Script: Tighten Permissions (after updates)

nano ~/wp_bash_scripts/6.tighten_permissions.sh
#!/bin/bash List directory contents of /var/www for reference sudo ls -l /var/www Prompt for the domain name read -p "Enter the domain name: " domain_name Check if the domain directory exists if [ ! -d "/var/www/$domain_name/public_html/" ]; then echo "Error: Domain directory /var/www/$domain_name/public_html/ does not exist." exit 1 fi Set permissions with confirmation echo "Setting permissions for /var/www/$domain_name/public_html/..." Grant read and execute permissions to the owner and group only sudo find "/var/www/$domain_name/public_html/" -type d -exec chmod 550 {} ; Grant read permissions to the owner and group only sudo find "/var/www/$domain_name/public_html/" -type f -exec chmod 440 {} ; Grant read, write and execute permissions to the owner and group only - wp-content/ sudo find "/var/www/$domain_name/public_html/wp-content" -type d -exec chmod 770 {} ; Grant read, write to the owner and group only - wp-content/ sudo find "/var/www/$domain_name/public_html/wp-content/" -type f -exec chmod 660 {} ; echo "Permissions hardened, no write permissions on core WordPress files and directories!"
angelscript
chmod +x ~/wp_bash_scripts/5.loosen_permissions.sh ~/wp_bash_scripts/6.tighten_permissions.sh

Monitoring Scripts

PHP Pool Memory Usage

nano ~/wp_bash_scripts/pool_list_usage.sh
#!/bin/bash Extract pool names pools=$(grep -E '^\suser\s=' /etc/php/8.3/fpm/pool.d/*.conf | awk -F= '{print $2}' | xargs | tr ' ' '\n' | sort -u) Display memory used by each pool for pool in $pools; do memory_used=$(ps -C php-fpm --user "$pool" -o rss= | awk '{ sum += $1; count++ } END { if (count > 0) printf ("%d%s\n", sum/NR/1024,"M") }') echo -e "Pool: $pool\nMemory Used: $memory_used\n" done
angelscript

OPcache File Count Monitor

nano ~/wp_bash_scripts/opcache_files.sh
#!/bin/bash echo "" echo "Checking for and displaying the opcache.max_accelerated_files directive value in each sites PHP pool file" echo "" POOL_DIR="/etc/php/8.3/fpm/pool.d/" for file in "$POOL_DIR"*.conf; do filename=$(basename "$file") if grep -q "^[^#;]*opcache.max_accelerated_files" "$file"; then value=$(grep "^[^#;]*opcache.max_accelerated_files" "$file" | awk -F'=' '{print $2}' | tr -d ' ') echo "File: $filename - opcache.max_accelerated_files: $value" else echo "File: $filename - opcache.max_accelerated_files directive not found" fi done echo "" echo "Listing sites and number of PHP files in each site, compare to the value set in the pool file" echo "" WWW_DIR="/var/www/" for domain_dir in "$WWW_DIR"*/ ; do if [ "$domain_dir" == "/var/www/html/" ]; then continue fi routeros domain_name=$(basename "$domain_dir") public_html_dir="$domain_dir/public_html/" if [ -d "$public_html_dir" ]; then php_file_count=$(find "$public_html_dir" -type f -name "*.php" | wc -l) echo "Domain: $domain_name - PHP files: $php_file_count" else echo "Domain: $domain_name - public_html directory not found" fi done echo ""
angelscript
chmod +x ~/wp_bash_scripts/pool_list_usage.sh ~/wp_bash_scripts/opcache_files.sh

📊 Complete Checklist

Pre-Flight Checklist

Step Task Status
1 DNS A & CNAME records configured
2 DNS propagation verified (ping test)
3 Bash scripts created and executable
4 Site directories created
5 Database created with credentials saved
6 Admin credentials generated and saved
7 Nginx server block configured
8 WordPress installed and configured
9 PHP-FPM pool created for site
10 File permissions hardened
11 PHP functions disabled
12 Open_basedir restrictions applied
13 Database privileges restricted
14 SSL certificate obtained
15 HTTPS enforced with redirects
16 HTTP/2 and HTTP/3 enabled
17 WordPress core optimizations applied
18 OPcache configured
19 FastCGI caching configured
20 Rate limiting enabled
21 SSL/TLS tested (SSL Labs)
22 HTTP/3 tested (HTTP3 Check)

🎯 Key Differences Between Sites

📌 Per-Site Unique Configuration:
  • PHP-FPM Pool: Each site must have unique pool name, socket path, and system user
  • FastCGI Cache: Each site must have unique cache path and keys_zone name
  • Nginx Server Block: Each site must have unique server_name and root directory
  • SSL Certificates: Each domain requires its own certificate (can use same cert for www subdomain)
  • Log Files: Each site should have separate access and error logs

⚠️ Common Pitfalls & Solutions

Issue Cause Solution
502 Bad Gateway PHP-FPM socket mismatch Verify socket path in Nginx config matches PHP pool
403 Forbidden Incorrect file permissions Check ownership and permissions on public_html
Database connection error Wrong credentials in wp-config.php Verify database name, user, and password match
SSL certificate error DNS not propagated or wrong webroot Verify DNS with ping, check webroot path in certbot command
Updates fail Permissions too restrictive Run loosen permissions script before updates
Cache not working Wrong cache zone name Ensure fastcgi_cache name matches keys_zone in nginx.conf

📚 Additional Resources

Testing Tools

  • SSL Labs: https://ssllabs.com/ - Comprehensive SSL/TLS analysis
  • HTTP/3 Check: https://http3check.net/ - Verify HTTP/3 support
  • GTmetrix: https://gtmetrix.com/ - Performance testing
  • WebPageTest: https://webpagetest.org/ - Detailed performance analysis

Security Testing

  • Qualys SSL Test: Verify SSL configuration security
  • Security Headers: https://securityheaders.com/ - Check HTTP security headers
  • Mozilla Observatory: https://observatory.mozilla.org/ - Overall security assessment

🎉 Congratulations!

You have successfully set up an additional WordPress site with enterprise-level security and performance optimization. Your site now benefits from:

  • ✅ Isolated PHP-FPM pool for enhanced security
  • ✅ Hardened file permissions and restricted PHP functions
  • ✅ SSL/TLS encryption with HTTP/2 and HTTP/3
  • ✅ Database privilege restrictions
  • ✅ FastCGI and OPcache optimization
  • ✅ Rate limiting protection against brute force attacks
  • ✅ Automated maintenance scripts for updates